"""
Decorator that caches a result for an object instance method inside
the object -- advantage of caching in the object is that we can
free up memory by garbage collecting object later

>>> class Bob(object):
...   base = 0
...   
...   def incr(self, *args, **kwargs):
...     self.base += 1
...     return self.base
...   
...   @method_memoize
...   def memoized_incr(self, *args, **kwargs):
...     return self.incr(*args, **kwargs)

Memoized values don't change
>>> b = Bob()
>>> b.memoized_incr()
1
>>> b.incr()
2
>>> b.incr()
3
>>> b.memoized_incr()
1

Changing args or kwargs yield different values
>>> b.memoized_incr('hello')
4
>>> b.memoized_incr('hello', hi=5)
5

Values are still cached though
>>> b.memoized_incr('hello')
4
>>> b.memoized_incr('hello', hi=5)
5

New instance object yields new values
>>> b2 = Bob()
>>> b2.incr()
1
>>> b2.memoized_incr()
2
>>> b2.memoized_incr()
2
>>> b.memoized_incr()
1

"""
from functools import wraps

def method_memoize(func):
  """ Memoize for methods -- stores inside class object
  """
  @wraps(func)
  def _decorated(self, *args, **kwargs):
    key = ( func, tuple(args), frozenset(kwargs.items()) )
    try:
      return getattr(self, '_memoized')[key]
    except AttributeError: # No _memoized dict
      self._memoized = {}
    except KeyError:
      pass
      
    self._memoized[key] = func(self, *args, **kwargs)
    return self._memoized[key]
  
  return _decorated
